package org.hawk.gwt.ppc.scanner.utils;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import org.hawk.gwt.ppc.javacc.JavaParserTreeConstants;
import org.hawk.gwt.ppc.javacc.JavaScanner;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.UniversalSourcesLoader;

/**
 * <p>Represents simple utility to load and parse java source/class files.
 * Single descriptor instance represents single source file.
 * Allows to get {@link JavaScope}s for different positions of scanner.</p>
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class JavaClassDescriptor {
	static final String CLASS_KEYWORD = "class";
	static final String INTERFACE_KEYWORD = "interface";
	static final String PACKAGE_KEYWORD = "package";
	static final String END_LINE_KEYWORD = ";";
	static final String IMPORT_KEYWORD = "import";
	static final String ENUM_KEYWORD = "enum";
	static final String ANNOTATION_KEYWORD_START = "@";
	static final String EXTENDS_KEYWORD = "extends";
	
	private SourcesLoader loader;
	private String className;
	private JavaScope compilationUnit;
	private JavaClassDescriptor arrayElement;
	/**
	 * Creates descriptor with default classpath source loader.
	 * @param className
	 * @throws IOException
	 * @throws ParseException
	 */
	JavaClassDescriptor(String className) throws IOException, ParseException{
		this(className, new UniversalSourcesLoader());
	}
	/**
	 * Creates desciptor with specific sources loader.
	 * @param className
	 * @param loader
	 * @throws ParseException
	 * @throws IOException
	 */
	JavaClassDescriptor(String className,SourcesLoader loader) throws ParseException, IOException{
		if (className == null) {
			throw new NullPointerException("className cannot be NULL");
		}
		if (loader == null) {
			throw new NullPointerException("loader cannot be NULL");
		}
		this.loader = loader;
		this.className = className;
		readClass(className);
	}
	private void readClass(String className)
			throws ParseException, IOException {
		if (!className.endsWith("[]")) {
			readClass();
			return;
		}
		className = className.substring(0, className.length() - 2);
		arrayElement = new JavaClassDescriptor(className, loader);
		compilationUnit = new ArraysTypeScope(arrayElement);
	}
	
	private void readClass() throws ParseException, IOException {
		CompilationUnitScanner scanner = new CompilationUnitScanner();
		InputStream stream = null;
		try {
			stream = getClassAsStream();
			scanner.startScan(stream, null);
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
	}
	
	private InputStream getClassAsStream() throws IOException {
		URL url = loader.getClass(className);
		if (url == null) {
			throw new IOException(
				"Unable to find source for class with name "+
				className+" using loader "+loader);
		}
		InputStream stream = url.openStream();
		return stream;
	}
	/**
	 * Creates class descriptor for already loaded class.
	 * All methods remain valid except for sources indices.
	 * TODO list:
	 * - disassembling 
	 * - debug lines from <code>.class</code> file.
	 * @param clazz
	 */
	JavaClassDescriptor(Class<?> clazz) {
		this.className = clazz.getName();
		this.compilationUnit = new JavaClassScope(clazz);
		if (clazz.isArray()) {
			arrayElement = new JavaClassDescriptor(clazz.getComponentType());
		}
	}
	/**
	 * @return name of the top-level class this descriptor is created for.
	 */
	public String getClassName() {
		return className;
	}
	/**
	 * @return top-level {@link JavaScope} object (representing top-level 
	 * class with the name descriptor created for).
	 * @see #getClassName()
	 */
	public JavaTypeScope getCompilationUnit() {
		return (JavaTypeScope) compilationUnit;
	}
	/**
	 * @param index (or -1 if index is unknown or have no meaning)
	 * @return scope object for the specific iterator index.
	 */
	public JavaScope getScope(int index) {
		return getCompilationUnit().getScope(index);
	}
	/**
	 * @return <code>true</code> if the class this descriptor is
	 * reflecting represents an array of elements.
	 */
	public boolean isArray() {
		return arrayElement != null;
	}
	/**
	 * @return class descriptor for an array element or
	 * <code>null</code> if this descriptor reflects non-array type.
	 */
	public JavaClassDescriptor getArrayElement() {
		return arrayElement;
	}

	/*
	 * Internal class for scanning java sources.
	 */
	private class CompilationUnitScanner extends JavaScanner implements Processor {

		private Stack<List<JavaVariable>> vars = new Stack<List<JavaVariable>>();
		private JavaScope currentScope;
		private JavaTypeScope currentTypeScope;
		
		private CompilationUnitScanner(){}

		@Override
		protected List<Processor> getPreprocessorsForKind(int kind) {
			return Collections.singletonList((Processor)this);
		}

		public void visitStart(TokenChain<? extends Token> iterator, int kind) throws ParseException, IOException {
			switch(kind){
			case JavaParserTreeConstants.JJTCOMPILATIONUNIT:
			case JavaParserTreeConstants.JJTBLOCK:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
			case JavaParserTreeConstants.JJTANNOTATIONTYPEDECLARATION:
			case JavaParserTreeConstants.JJTENUMDECLARATION:
			case JavaParserTreeConstants.JJTCLASSORINTERFACEDECLARATION:
				openScope(iterator,kind);
				return;
			case JavaParserTreeConstants.JJTENUMBODY:
			case JavaParserTreeConstants.JJTANNOTATIONTYPEBODY:
			case JavaParserTreeConstants.JJTCLASSORINTERFACEBODY:
				setTypeName(iterator);
				return;
			case JavaParserTreeConstants.JJTFIELDDECLARATION:
				vars.peek().add(readVar(iterator));
				return;
			case JavaParserTreeConstants.JJTCONSTRUCTORDECLARATION:
				openConstructorScope(iterator);
				return;
			case JavaParserTreeConstants.JJTVARIABLEDECLARATORID:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
				iterator.next();
				vars.peek().add(readVar(iterator));
				return;
			}
		}

		private void processEnumConstant(TokenChain<? extends Token> iterator) {
			String name = iterator.current().getToken();
			int index = iterator.index();
			String type = ((JavaTypeScope)currentScope).getResolvedClassName();
			JavaVariable result = new JavaVariable(name, type, index, currentScope);
			List<JavaModifier> modifiers = new LinkedList<JavaModifier>();
			modifiers.add(JavaModifier.PUBLIC);
			modifiers.add(JavaModifier.FINAL);
			modifiers.add(JavaModifier.STATIC);
			result.setModifiers(modifiers);
			vars.peek().add(result);
		}

		private void openScope(TokenChain<? extends Token> iterator, int kind) {
			JavaScope old = currentScope;
			int index = iterator==null?0:iterator.index();
			if (old == null){
				currentTypeScope = new JavaTypeScope(null, 
						index, addVarsToStack(),loader);
				compilationUnit = currentTypeScope;
				currentScope = currentTypeScope;
				return;
			}
			if (kind == JavaParserTreeConstants.JJTCLASSORINTERFACEDECLARATION || 
				kind == JavaParserTreeConstants.JJTANNOTATIONTYPEDECLARATION ||
				kind == JavaParserTreeConstants.JJTENUMDECLARATION) {
				// reference comparison is not a mistake:
				// instance is the same
				if (currentTypeScope == currentScope) {
					return;
				}
				currentTypeScope = new JavaTypeScope(old, index, addVarsToStack(), loader);
				currentScope = currentTypeScope;
			} else {
				currentScope = new JavaScope(old,index,addVarsToStack());
			}
			old.addChild(currentScope);
		}

		private List<JavaVariable> addVarsToStack() {
			List<JavaVariable> varsList = new LinkedList<JavaVariable>();
			vars.push(varsList);
			return varsList;
		}

		private void setTypeName(TokenChain<? extends Token> iterator) throws ParseException, IOException {
			if(currentTypeScope != currentScope){
				openAnonimousTypeScope(iterator);
				return;
			}
			while(!iterator.previous().getToken().equals(CLASS_KEYWORD) &&
					!iterator.current().getToken().equals(INTERFACE_KEYWORD) &&
					!iterator.current().getToken().equals(ENUM_KEYWORD) &&
					!iterator.current().getToken().equals(ANNOTATION_KEYWORD_START));
			currentTypeScope.setModifiers(readModifiers(iterator));

			if (iterator.current().getToken().equals(ANNOTATION_KEYWORD_START)) {
				iterator.next();
				currentTypeScope.setEntityType(ANNOTATION_KEYWORD_START);
			} else {
				currentTypeScope.setEntityType(iterator.current().getToken());
			}
			currentTypeScope.setClassName(iterator.next().getToken());
			readTypeParameters(iterator);
			// clear for top-level anonymous classes
			currentTypeScope = null;
		}

		private void readTypeParameters(TokenChain<? extends Token> iterator) throws ParseException, IOException {
			if (!iterator.hasNext()) {
				return;
			}
			if (!iterator.next().getToken().equals("<")) {
				iterator.previous();
				return;
			}
			readSingleTypeParameter(iterator);
			
			int deep = 1;
			while (deep > 0) {
				String token = iterator.current().getToken();
				if (token.length() != 1) {
					iterator.next();
					continue;
				}
				char tok = token.charAt(0);
				if (tok == '<') {
					deep++;
					iterator.next();
				} else if (tok == '>') {
					deep--;
					iterator.next();
				} else if (deep == 1 && tok == ',') {
					readSingleTypeParameter(iterator);
				} else {
					iterator.next();
				}
			}
		}
		
		private void readSingleTypeParameter(TokenChain<? extends Token> iterator) throws ParseException, IOException {
			String name = iterator.next().getToken();
			List<String> bounds = new ArrayList<String>(1);
			if (!iterator.next().getToken().equals(EXTENDS_KEYWORD)) {
				iterator.previous();
			} else {
				do {
					int deep = 0;
					String tok = iterator.next().getToken();
					while (deep > 0 || (!tok.equals("&") && !tok.equals(",") && !tok.equals(">"))) {
						if (tok.equals(">")) {
							deep--;
						} else if (tok.equals("<")) {
							deep++;
						}
						tok = iterator.next().getToken();
					}
					int index = iterator.index();
					bounds.add(readType(iterator));
					iterator.moveTo(index - 1);
				} while (iterator.next().getToken().equals("&"));
			}
			if (currentScope instanceof JavaTypeScope) {
				currentTypeScope.addTypeParameter(name, bounds);
			} else if (currentScope instanceof JavaMethod) {
				((JavaMethod)currentScope).addTypeParameter(name, bounds);
			} else {
				throw new ParseException("Should be ");
			}
		}

		private void openAnonimousTypeScope(TokenChain<? extends Token> iterator) throws ParseException {
			int deep = 1;
			
			while(deep > 0){
				if (iterator.previous().getToken().equals("}")) {
					while(deep > 0){
						if(iterator.previous().getToken().equals("{")){
							deep--;
						} else if(iterator.current().getToken().equals("}")){
							deep++;
						}
					}
					iterator.previous();
					deep = 1;
				} else if(iterator.current().getToken().equals("(")){
					deep--;
				} else if(iterator.current().getToken().equals(")")){
					deep++;
				}
			}
			
			String type = readType(iterator);
			JavaTypeScope newScope = new JavaTypeScope(currentScope, iterator.index(), addVarsToStack(), loader);
			newScope.markAnonymous();
			currentScope.addChild(newScope);
			
			JavaClassDescriptor descriptor = getCompilationUnit().resolveTypeDescriptor(type);
			if (descriptor == null) {
				// TODO: need more verbose exception here to have more precise logging.
				throw new ParseException("Unable to resolve class with name '" + type + 
					"'. It appears you have unresolved compilation problems or this type is not supported by GWT.");
			}
			JavaTypeScope typeScope = descriptor.getCompilationUnit();
			if(typeScope.isInterface()){
				newScope.addImplementedInterface(type);
			}
			else {
				newScope.setExtendedClass(type);
			}
			currentScope = newScope;
		}

		private JavaVariable readVar(TokenChain<? extends Token> iterator) throws ParseException {
			String name = iterator.current().getToken();
			int index = iterator.index();
			String type = readType(iterator);
			JavaVariable result = new JavaVariable(name, type, index, currentScope);
			result.setModifiers(readModifiers(iterator));
			return result;
		}

		private void skipPackage(TokenChain<? extends Token> iterator) {
			while(iterator.previous().getToken().equals(".")){
				iterator.previous();
			}
			iterator.next();
		}

		private void skipTypeParameters(TokenChain<? extends Token> iterator) {
			if(!iterator.previous().getToken().equals(">")){
				return;
			}
			int deep = 1;
			while(deep>0){
				if(iterator.previous().getToken().equals(">"))deep++;
				else if(iterator.current().getToken().equals("<"))deep--;
			}
			iterator.previous();
		}

		public void visitEnd(TokenChain<? extends Token> iterator, int kind) throws ParseException, IOException {
			switch(kind){
			case JavaParserTreeConstants.JJTCONSTRUCTORDECLARATION:
				closeConstructorScope(iterator);
			case JavaParserTreeConstants.JJTBLOCK:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
			case JavaParserTreeConstants.JJTCLASSORINTERFACEBODY:
			case JavaParserTreeConstants.JJTANNOTATIONTYPEBODY:
			case JavaParserTreeConstants.JJTENUMBODY:
			case JavaParserTreeConstants.JJTMETHODDECLARATION:
				currentScope.endScope(iterator.index());
				currentScope = currentScope.getParent();
				vars.pop();
				return;
			case JavaParserTreeConstants.JJTIMPORTDECLARATION:
				while(!iterator.previous().getToken().equals(IMPORT_KEYWORD));
				StringBuilder imp = new StringBuilder();
				while(!iterator.next().getToken().equals(END_LINE_KEYWORD)){
					imp.append(iterator.current().getToken());
				}
				((JavaTypeScope)currentScope).addImport(imp.toString());
				return;
			case JavaParserTreeConstants.JJTENUMCONSTANT:
				processEnumConstant(iterator);
			case JavaParserTreeConstants.JJTPACKAGEDECLARATION:
				while(!iterator.previous().getToken().equals(PACKAGE_KEYWORD));
				StringBuilder pkg = new StringBuilder();
				while(!iterator.next().getToken().equals(END_LINE_KEYWORD)){
					pkg.append(iterator.current().getToken());
				}
				((JavaTypeScope)currentScope).setCurrentPackage(pkg.toString());
				return;
			case JavaParserTreeConstants.JJTMETHODDECLARATOR:
				readMethod(iterator);
				return;
			case JavaParserTreeConstants.JJTEXTENDSLIST:
				iterator.next();
				((JavaTypeScope)currentScope).setExtendedClass(
						readType(iterator));
				return;
			case JavaParserTreeConstants.JJTIMPLEMENTSLIST:
				iterator.next();
				do {
					((JavaTypeScope)currentScope).addImplementedInterface(
						readType(iterator));
				}
				while(!iterator.previous().getToken().equals("implements"));
				return;
			}
		}

		private void openConstructorScope(TokenChain<? extends Token> iterator) throws ParseException, IOException {
			 JavaTypeScope classScope = (JavaTypeScope)currentScope;
			 currentScope = new JavaConstructor(classScope.getClassName(), classScope, iterator.index(), addVarsToStack(), false);
			 skipTypeParameters(iterator);
			 iterator.next();
			 currentScope.setModifiers(readModifiers(iterator));
			 iterator.previous();
			 readTypeParameters(iterator);
			 classScope.addChild(currentScope);
		}

		private void closeConstructorScope(TokenChain<? extends Token> iterator) throws ParseException {
			JavaConstructor c = (JavaConstructor)currentScope;
			int index = iterator.index();
			int deep = 1;
			iterator.next();
			while (deep > 0) {
				if (iterator.previous().getToken().equals("{")) {
					deep--;
				} else if (iterator.current().getToken().equals("}")) {
					deep++;
				}
			}
			while(!iterator.previous().getToken().equals(")"));
			List<JavaVariable> parameters = readMethodParameters(iterator);
			List<JavaVariable> vars = this.vars.peek();
			for(JavaVariable var : parameters){
				var.setScope(c);
				vars.add(0, var);
				c.addParameter(var.getTypeName());
				c.addRawParameter(var.getRawTypeName());
			}
			iterator.moveTo(index);
			
			((JavaTypeScope)currentScope.getParent()).addConstructor(c);
		}

		private void readMethod(TokenChain<? extends Token> iterator) throws ParseException, IOException {
			if (isAnonymousScope() && currentScope.getStart() >= iterator.index()) {
				/*
				 * Some bug (most probably in javacc):
				 */
				while(!iterator.next().getToken().equals("("));
				int deep = 1; 
				while(deep > 0) {
					if (iterator.next().getToken().equals("(")) {
						deep++;
					} else if (iterator.current().getToken().equals(")")) {
						deep--;
					}
				}
				while(!iterator.next().getToken().equals(")"));
			}
			List<JavaVariable> parameters = readMethodParameters(iterator);
			vars.push(parameters);
			boolean varArg = !parameters.isEmpty() && parameters.get(parameters.size() - 1).isVarArg();
			String name = iterator.previous().getToken();
			JavaTypeScope classScope = (JavaTypeScope)currentScope;
			currentScope = new JavaMethod(name, classScope, iterator.index(), parameters, readType(iterator), varArg);
			skipTypeParameters(iterator);
			iterator.next();
			currentScope.setModifiers(readModifiers(iterator));
			iterator.previous();
			
			readTypeParameters(iterator);
			iterator.next();
			classScope.addChild(currentScope);
		}

		private boolean isAnonymousScope() {
			return currentScope instanceof JavaTypeScope && ((JavaTypeScope)currentScope).isAnonymous();
		}
		
		private List<JavaModifier> readModifiers(TokenChain<? extends Token> iterator) {
			int index = iterator.index();
			List<JavaModifier> result = new ArrayList<JavaModifier>();
			JavaModifier mod = JavaModifier.resolve(iterator.previous().getToken());
			while (mod != null) {
				result.add(mod);
				mod = JavaModifier.resolve(iterator.previous().getToken());
			}
			iterator.moveTo(index);
			return result;
		}

		private String readType(TokenChain<? extends Token> iterator) throws ParseException {
			int index = iterator.index();
			skipArrayDefinition(iterator);
			skipTypeParameters(iterator);
			skipPackage(iterator);
			int next = iterator.index();
			StringBuilder type = new StringBuilder();
			while (iterator.index() < index) {
				String current = iterator.current().getToken();
				type.append(current);
				if (current.equals("<")) {
					appendParams(type, iterator);
				}
				iterator.next();
			}
			iterator.moveTo(next);
			return type.toString();
		}

		private void appendParams(StringBuilder type, TokenChain<? extends Token> iterator) {
			int deep = 1;
			while (deep > 0) {
				String tok = iterator.next().getToken();
				if (tok.equals("<")) {
					deep ++;
				} else if (tok.equals(">")) {
					deep --;
				} else if (tok.equals("extends") || tok.equals("super")) {
					type.append(' ');
					type.append(tok);
					type.append(' ');
					continue;
				}
				type.append(tok);
			}
		}

		private void skipArrayDefinition(TokenChain<? extends Token> iterator) throws ParseException {
			while (true) {
				if(!iterator.previous().getToken().equals("]")){
					iterator.next();
					if(!iterator.current().getToken().equals("]")) {
						return;
					}
				}
				if(!iterator.previous().getToken().equals("[")){
					throw new ParseException("Java syntax error - wrong array definition");
				}
			}
		}

		private List<JavaVariable> readMethodParameters(TokenChain<? extends Token> iterator) throws ParseException {
			List<JavaVariable> result = new LinkedList<JavaVariable>();
			while(!iterator.previous().getToken().equals("(")){
				if(!result.isEmpty())iterator.previous();
				result.add(0, readParameter(iterator));
			}
			return result;
		}
		
		private JavaVariable readParameter(TokenChain<? extends Token> iterator) throws ParseException{
			String name = iterator.current().getToken();
			boolean varArg = isVarArg(iterator);
			int index = iterator.index();
			StringBuilder type = new StringBuilder();
			type.append(readType(iterator));
			if (varArg) {
				type.append("[]");
			}
			List<JavaModifier> modifiers = new ArrayList<JavaModifier>();
			JavaModifier mod = JavaModifier.resolve(iterator.previous().getToken());
			while (mod != null) {
				modifiers.add(mod);
				mod = JavaModifier.resolve(iterator.previous().getToken());
			}
			
			skipParameterAnnotation(iterator);

			JavaVariable result = new JavaVariable(name, type.toString(), index, currentScope, varArg);
			result.setModifiers(modifiers);
			return result;
		}

		private void skipParameterAnnotation(TokenChain<? extends Token> iterator) {
			if (!iterator.current().getToken().equals(")")) {
				iterator.next();
				return;
			}
			int deep = 1;
			
			while (deep > 0) {
				if (iterator.previous().getToken().equals("(")) { 
					deep--;
				} else if (iterator.current().getToken().equals(")")) { 
					deep++;
				}
			}
			iterator.previous();
			iterator.previous();
		}

		private boolean isVarArg(TokenChain<? extends Token> iterator) {
			if(!iterator.previous().getToken().equals("...")){
				iterator.next();
				return false;
			}
			return true;
		}
	}
}
